Skip to content

feat(storage): Postgres daily partitioning + DROP-PARTITION retention#53

Merged
aksOps merged 1 commit into
mainfrom
feat/postgres-partitioning
Apr 27, 2026
Merged

feat(storage): Postgres daily partitioning + DROP-PARTITION retention#53
aksOps merged 1 commit into
mainfrom
feat/postgres-partitioning

Conversation

@aksOps

@aksOps aksOps commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Opt-in Postgres declarative-partitioning adapter for the logs table, gated by DB_POSTGRES_PARTITIONING=daily. Default remains the legacy unpartitioned schema.
  • New PartitionScheduler runs hourly: ensures today + DB_PARTITION_LOOKAHEAD_DAYS future daily partitions exist, drops partitions whose entire range predates the retention cutoff (DROP TABLE — orders of magnitude faster than DELETE).
  • RetentionScheduler reads repo.LogsPartitioned() and skips the logs DELETE branch when partitioning is active. traces and metric_buckets continue to use the existing batched DELETE path.
  • pg_trgm GIN indexes are declared on the parent and propagate automatically to current/future child partitions (Postgres ≥ 11).
  • New telemetry: otelcontext_partitions_dropped_total and otelcontext_partitions_active.

Why

Phase 3b + Phase 5 of the 7-day-retention robustness initiative. Row-level DELETE at 7-day × 100–200 services becomes the dominant DB cost at scale; DROP PARTITION turns retention into an ~instant DDL. Postgres-only and opt-in per the board ruling — SQLite remains the zero-config default.

Greenfield only

Startup refuses to enable partitioning if logs already exists as a non-partitioned table. Migrating an unpartitioned logs to partitioned requires data movement and is out of scope for this phase. The greenfield guard is enforced and tested.

Test plan

  • go test ./... -race -count=1 — full unit suite green
  • sudo go test -tags=integration ./internal/storage/ -run TestPGPartition — 6/6 partitioning integration tests pass against postgres:16-alpine via testcontainers
    • TestPGPartition_LogsTableIsPartitioned — verifies relkind=p and ≥5 initial partitions
    • TestPGPartition_InsertRoutesToCorrectChild — verifies row-routing to today's partition
    • TestPGPartition_DropExpired — verifies expired partition is dropped, today's stays
    • TestPGPartition_GreenfieldGuard — verifies refusal on existing non-partitioned logs
    • TestPGPartition_SchedulerDropsExpiredAndCreatesLookahead — full scheduler lifecycle
    • TestPGPartition_PgTrgmIndexesPropagateToChildren — verifies GIN inheritance
  • TestPG_ILIKE_CaseInsensitiveSearch, TestPG_Bytea_CompressedTextRoundTrip, TestPG_VacuumAnalyze_OutsideTx — pre-existing PG paths still pass
  • 4 unit tests for partitionNameForDay, quoteIdent, parsePartitionUpper
  • go vet ./... and golangci-lint run --new-from-rev=origin/main — clean

🤖 Generated with Claude Code

Adds an opt-in Postgres declarative-partitioning adapter for the `logs`
table, gated by `DB_POSTGRES_PARTITIONING=daily` (default off). When
enabled, `logs` is provisioned as a `RANGE PARTITION BY (timestamp)`
parent with the composite PK `(id, timestamp)`; AutoMigrate skips the
model and a `PartitionScheduler` runs hourly to (a) ensure today +
lookahead future daily partitions exist and (b) DROP partitions whose
range predates the retention cutoff. DROP PARTITION beats row-level
DELETE for retention by orders of magnitude.

`RetentionScheduler` reads `repo.LogsPartitioned()` and skips the
`logs` DELETE branch when partitioning is active so we never run two
retention paths against the same table.

Greenfield only — startup refuses to enable partitioning if `logs`
already exists as a non-partitioned table. Migrating an unpartitioned
`logs` to partitioned requires data movement and is out of scope.

`pg_trgm` GIN indexes are declared on the parent and propagate
automatically to current/future child partitions (Postgres ≥ 11).

New telemetry: `otelcontext_partitions_dropped_total` (counter) and
`otelcontext_partitions_active` (gauge). New env vars validated at
config load: `DB_POSTGRES_PARTITIONING` ("", none, daily) and
`DB_PARTITION_LOOKAHEAD_DAYS` (0..365). Daily mode is rejected when
DB_DRIVER != postgres.

Tests: 6 Postgres integration tests (testcontainers, auto-skipped
without docker) covering the partitioned schema, child-partition
routing, DROP_EXPIRED, greenfield guard, scheduler lifecycle, and
pg_trgm GIN inheritance. 4 unit tests for the date/identifier helpers.
Docs updated in CLAUDE.md and OPERATIONS.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
4.4% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@aksOps aksOps merged commit 436201e into main Apr 27, 2026
16 of 17 checks passed
@aksOps aksOps deleted the feat/postgres-partitioning branch April 27, 2026 16:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant